package semantics;

import ic.LanguageException;
import ic.ast.DeclClass;
import ic.ast.DeclField;
import ic.ast.DeclMethod;
import ic.ast.DeclStaticMethod;
import ic.ast.DeclVirtualMethod;
import ic.ast.Expression;
import ic.ast.HasExpressions;
import ic.ast.HasName;
import ic.ast.HasType;
import ic.ast.LocalVariable;
import ic.ast.Node;
import ic.ast.Parameter;
import ic.ast.Program;
import ic.ast.StmtBlock;
import ic.ast.Type;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;

public class Scope {
	private final Scope parent;

	private HashMap<String, Node> symbol_table = new HashMap<String, Node>();
	private HashSet<Node> from_inheritance = new HashSet<Node>();

	final public Node owner;
	final private List<Scope> children = new ArrayList<Scope>();

	private Scope(Node owner) {
		parent = null;
		this.owner = owner;
	}

	private Scope(Scope parent, Node owner) {
		this.parent = parent;
		this.owner = owner;
	}

	public static Scope makeNewScopeTree(Node owner) {
		return new Scope(owner);
	}

	public Scope makeChildScope(Node child_owner, Scope parent) {
		Scope res = new Scope(parent, child_owner);
		this.children.add(res);
		return res;
	}

	public Scope getParent() {
		return parent;
	}

	public Scope addAndExtend(Node nd) {
		addToScope(nd);
		return makeChildScope(nd, this);
	}

	public Scope addAndExtend(Node nd, Scope inherit_scope_of) {
		Scope res = addAndExtend(nd);

		res.symbol_table.putAll(inherit_scope_of.symbol_table);

		for (Node in : inherit_scope_of.symbol_table.values()) {
			res.from_inheritance.add(in);
		}

		return res;
	}

	public void addToScope(Node nd) {
		if (nd instanceof HasName) {
			addToSymbolTable(((HasName) nd).getName(), nd);
		}

		nd.setScope(this);

		if (nd instanceof HasType) {
			addToScope(((HasType) nd).getType());
		}

		if (nd instanceof HasExpressions) {
			for (Expression e : ((HasExpressions) nd).getExpressions()) {
				addToScope(e);
			}
		}
	}

	// nd must implement HasName
	private void addToSymbolTable(String name, Node nd) {
		if (symbol_table.containsKey(name)) {
			boolean flgFoundBadParameter = false;

			if (symbol_table.get(name) instanceof DeclMethod
					&& nd instanceof DeclMethod
					&& from_inheritance.contains(symbol_table.get(name))) {
				// special case for overriding
				DeclMethod oldM = (DeclMethod) symbol_table.get(name);
				DeclMethod newM = (DeclMethod) nd;
				if (oldM.getType().equals(newM.getType())) {
					if (oldM.getFormals().size() == newM.getFormals().size()) {
						for (int i = 0; i < oldM.getFormals().size(); ++i) {
							Parameter p1 = oldM.getFormals().get(i);
							Parameter p2 = newM.getFormals().get(i);

							if (!p1.getType().equals(p2.getType())) {
								flgFoundBadParameter = true;
								break;
							}
						}

						if (!flgFoundBadParameter) {
							symbol_table.put(name, nd);
							return;
						}
					}
				}
			}

			throw new LanguageException(nd.getLine(), makeShadowMessage(name,
					nd, symbol_table.get(name), flgFoundBadParameter));
		} else {
			symbol_table.put(name, nd);
		}
	}

	private String makeShadowMessage(String name, Node new_nd,
			Node existing_nd, boolean flgFoundBadParameter) {
		if (flgFoundBadParameter) {
			return "method '" + name
					+ "' overloads a different method with the same name";
		}

		if (from_inheritance.contains(existing_nd)
				|| !new_nd.getClass().isAssignableFrom(existing_nd.getClass())) {
			String type1 = ((HasName) new_nd).getFunctionalJobDescription();
			String type2 = ((HasName) existing_nd)
					.getFunctionalJobDescription();

			return type1 + " " + name + " is shadowing a " + type2
					+ " with the same name";
		} else
			return "Id " + name + " already defined in current scope";
	}

	public Node getFlat(String name) {
		return symbol_table.get(name);
	}

	public Node getRecursive(String name) {
		for (Scope curr = this; curr != null; curr = curr.getParent()) {
			if (curr.symbol_table.containsKey(name))
				return curr.symbol_table.get(name);
		}

		return null;
	}

	public String prettyPrint(boolean isLibInput, Node lib_ast) {
		StringBuilder sb = new StringBuilder();

		flgPrintLibrary = isLibInput;
		library_root = lib_ast;

		prettyPrint_base(sb);

		return sb.toString();
	}

	boolean flgPrintLibrary = false;

	Node library_root = null;

	private void prettyPrint_base(StringBuilder sb) {
		HasName hn = null;
		DeclClass inherits_from = null;

		if (owner instanceof HasName)
			hn = (HasName) owner;

		sb.append("\n");

		if (owner instanceof Program)
			sb.append("Global Symbol Table\n");

		else if (owner instanceof DeclClass) {
			sb.append("Class Symbol Table:\t" + hn.getName());

			if (((DeclClass) owner).hasSuperClass()) {
				sb.append("\t");
				sb.append("(parent = "
						+ ((DeclClass) owner).getSuperClassName() + ")");

				inherits_from = (DeclClass) owner;
			}

			sb.append("\n");
		}

		else if (owner instanceof DeclMethod) {
			sb.append("Method Symbol Table:\t" + hn.getName() + "\t(parent = "
					+ getParentName() + ")\n");
		}

		else if (owner instanceof StmtBlock) {
			sb.append("Statement Block Symbol Table: @" + getLastHasName()
					+ "  (parent = " + getParentName() + ")\n");
		}

		List<Entry<String, Node>> entries = new ArrayList<Entry<String, Node>>(
				symbol_table.entrySet());

		Collections.sort(entries, new Comparator<Entry<String, Node>>() {

			@Override
			public int compare(Entry<String, Node> o1, Entry<String, Node> o2) {
				// comparison by nodes:
				final Node n1 = o1.getValue();
				final Node n2 = o2.getValue();

				int type_order = val(n1) - val(n2);
				if (type_order != 0)
					return type_order;

				return o1.getKey().compareTo(o2.getKey());
			}

			private int val(Node n) {
				if (n instanceof DeclField)
					return 0;
				else if (n instanceof DeclStaticMethod)
					return 1;
				else if (n instanceof DeclVirtualMethod)
					return 2;
				else if (n instanceof Parameter)
					return 3;
				else if (n instanceof LocalVariable)
					return 4;

				return 5;
			}

		});

		// Finished header, now table
		for (Entry<String, Node> entry : entries) {
			final String name = entry.getKey();
			final HasName hs = (HasName) entry.getValue();

			if (!flgPrintLibrary && library_root != null
					&& hs.equals(library_root))
				continue;

			if (inherits_from != null) {
				if (this.from_inheritance.contains(hs))
					continue;
			}

			sb.append("\t");
			sb.append(hs.getPrettyDescription() + ":\t" + name);

			if (hs instanceof HasType) {
				sb.append(" : ");

				if (hs instanceof DeclMethod) {
					List<Parameter> params = ((DeclMethod) hs).getFormals();
					if (params.isEmpty())
						sb.append("void");
					else {
						boolean flgFirst = true;
						for (Parameter p : params) {
							if (flgFirst)
								flgFirst = false;
							else
								sb.append(", ");
							sb.append(getTypePrint(p.getType()));
						}
					}

					sb.append(" -> ");
				}

				sb.append(getTypePrint(((HasType) hs).getType()));
			}

			sb.append("\n");
		}

		List<Scope> for_display = (this.owner instanceof Program) ? mergeScopesByOwner(new ArrayList<Scope>(
				children)) : children;

		for (Scope child : for_display) {
			if (!flgPrintLibrary && library_root != null
					&& child.owner.equals(library_root))
				continue;
			child.prettyPrint_base(sb);
		}
	}

	private static List<Scope> mergeScopesByOwner(List<Scope> scopes) {
		int len = scopes.size();
		for (int i = 0; i < len; ++i) {

			final Scope curr1 = scopes.get(i);
			if (curr1 == null)
				continue;
			for (int j = i + 1; j < len; ++j) {
				final Scope curr2 = scopes.get(j);
				if (curr2 == null)
					continue;
				if (curr1.owner.equals(curr2.owner)) {
					// merge
					curr1.symbol_table.putAll(curr2.symbol_table);
					curr1.children.addAll(curr2.children);
					scopes.remove(j);
					scopes.add(j, null);
				}
			}
		}

		List<Scope> res = new ArrayList<Scope>();
		for (int i = 0; i < len; ++i) {
			final Scope curr = scopes.get(i);
			if (curr == null)
				continue;
			res.add(curr);
		}

		return res;
	}

	private static String getTypePrint(Type t) {
		String res = t.getDisplayName();
		for (int i = 0; i < t.getArrayDimension(); ++i) {
			res = res + "[]";
		}
		return res;
	}

	private String getParentName() {
		if (parent == null)
			return "";

		if (parent != null && parent.owner instanceof HasName)
			return ((HasName) parent.owner).getName();
		else
			return "@" + parent.getParentName();
	}

	private String getLastHasName() {
		if (owner instanceof HasName)
			return ((HasName) owner).getName();
		else {
			if (parent != null)
				return parent.getLastHasName();
			else
				return "";
		}
	}
}
